//-----------------------------------------------------------------------------
// [SECTION] Header mess
//-----------------------------------------------------------------------------

#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif

#include "sweety.h"

#ifndef SW_DEFINE_MATH_OPERATORS
#define SW_DEFINE_MATH_OPERATORS
#endif
#include "sweety_Internal.h"


//-------------------------------------------------------------------------
// [SECTION] Tables: Settings (.ini data)
//-------------------------------------------------------------------------
// FIXME: The binding/finding/creating flow are too confusing.
//-------------------------------------------------------------------------
// - TableSettingsInit() [Internal]
// - TableSettingsCalcChunkSize() [Internal]
// - TableSettingsCreate() [Internal]
// - TableSettingsFindByID() [Internal]
// - TableGetBoundSettings() [Internal]
// - TableResetSettings()
// - TableSaveSettings() [Internal]
// - TableLoadSettings() [Internal]
// - TableSettingsHandler_ClearAll() [Internal]
// - TableSettingsHandler_ApplyAll() [Internal]
// - TableSettingsHandler_ReadOpen() [Internal]
// - TableSettingsHandler_ReadLine() [Internal]
// - TableSettingsHandler_WriteAll() [Internal]
// - TableSettingsInstallHandler() [Internal]
//-------------------------------------------------------------------------
// [Init] 1: TableSettingsHandler_ReadXXXX()   Load and parse .ini file into TableSettings.
// [Main] 2: TableLoadSettings()               When table is created, bind Table to TableSettings, serialize TableSettings data into Table.
// [Main] 3: TableSaveSettings()               When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty.
// [Main] 4: TableSettingsHandler_WriteAll()   When .ini file is dirty (which can come from other source), save TableSettings into .ini file.
//-------------------------------------------------------------------------

// Clear and initialize empty settings instance
static void TableSettingsInit(SwGuiTableSettings* settings, SwGuiID id, int columns_count, int columns_count_max)
{
    SW_PLACEMENT_NEW(settings) SwGuiTableSettings();
    SwGuiTableColumnSettings* settings_column = settings->GetColumnSettings();
    for (int n = 0; n < columns_count_max; n++, settings_column++)
        SW_PLACEMENT_NEW(settings_column) SwGuiTableColumnSettings();
    settings->ID = id;
    settings->ColumnsCount = (SwGuiTableColumnIdx)columns_count;
    settings->ColumnsCountMax = (SwGuiTableColumnIdx)columns_count_max;
    settings->WantApply = true;
}

static void TableSettingsHandler_ClearAll(SwGuiContext* ctx, SwGuiSettingsHandler*)
{
    SwGuiContext& g = *ctx;
    for (int i = 0; i != g.Tables.GetMapSize(); i++)
        if (SwGuiTable* table = g.Tables.TryGetMapData(i))
            table->SettingsOffset = -1;
    g.SettingsTables.clear();
}

static void* TableSettingsHandler_ReadOpen(SwGuiContext*, SwGuiSettingsHandler*, const char* name)
{
    SwGuiID id = 0;
    int columns_count = 0;
    if (sscanf(name, "0x%08X,%d", &id, &columns_count) < 2)
        return NULL;

    if (SwGuiTableSettings* settings = SW::TableSettingsFindByID(id))
    {
        if (settings->ColumnsCountMax >= columns_count)
        {
            TableSettingsInit(settings, id, columns_count, settings->ColumnsCountMax); // Recycle
            return settings;
        }
        settings->ID = 0; // Invalidate storage, we won't fit because of a count change
    }
    return SW::TableSettingsCreate(id, columns_count);
}

static void TableSettingsHandler_ReadLine(SwGuiContext*, SwGuiSettingsHandler*, void* entry, const char* line)
{
    // "Column 0  UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v"
    SwGuiTableSettings* settings = (SwGuiTableSettings*)entry;
    float f = 0.0f;
    int column_n = 0, r = 0, n = 0;

    if (sscanf(line, "RefScale=%f", &f) == 1) { settings->RefScale = f; return; }

    //if (sscanf(line, "Column %d%n", &column_n, &r) == 1)
    //{
    //    if (column_n < 0 || column_n >= settings->ColumnsCount)
    //        return;
    //    line = SwStrSkipBlank(line + r);
    //    char c = 0;
    //    SwGuiTableColumnSettings* column = settings->GetColumnSettings() + column_n;
    //    column->Index = (SwGuiTableColumnIdx)column_n;
    //    if (sscanf(line, "UserID=0x%08X%n", (SwU32*)&n, &r) == 1) { line = ImStrSkipBlank(line + r); column->UserID = (SwGuiID)n; }
    //    if (sscanf(line, "Width=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = (float)n; column->IsStretch = 0; settings->SaveFlags |= SwGuiTableFlags_Resizable; }
    //    if (sscanf(line, "Weight=%f%n", &f, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = f; column->IsStretch = 1; settings->SaveFlags |= SwGuiTableFlags_Resizable; }
    //    if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->IsEnabled = (SwU8)n; settings->SaveFlags |= SwGuiTableFlags_Hideable; }
    //    if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (SwGuiTableColumnIdx)n; settings->SaveFlags |= SwGuiTableFlags_Reorderable; }
    //    if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (SwGuiTableColumnIdx)n; column->SortDirection = (c == '^') ? SwGuiSortDirection_Descending : SwGuiSortDirection_Ascending; settings->SaveFlags |= SwGuiTableFlags_Sortable; }
    //}
}

// Apply to existing windows (if any)
static void TableSettingsHandler_ApplyAll(SwGuiContext* ctx, SwGuiSettingsHandler*)
{
    SwGuiContext& g = *ctx;
    for (int i = 0; i != g.Tables.GetMapSize(); i++)
        if (SwGuiTable* table = g.Tables.TryGetMapData(i))
        {
            table->IsSettingsRequestLoad = true;
            table->SettingsOffset = -1;
        }
}

static void TableSettingsHandler_WriteAll(SwGuiContext* ctx, SwGuiSettingsHandler* handler, SwGuiTextBuffer* buf)
{
    SwGuiContext& g = *ctx;
    for (SwGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings))
    {
        if (settings->ID == 0) // Skip ditched settings
            continue;

        // TableSaveSettings() may clear some of those flags when we establish that the data can be stripped
        // (e.g. Order was unchanged)
        const bool save_size = (settings->SaveFlags & SwGuiTableFlags_Resizable) != 0;
        const bool save_visible = (settings->SaveFlags & SwGuiTableFlags_Hideable) != 0;
        const bool save_order = (settings->SaveFlags & SwGuiTableFlags_Reorderable) != 0;
        const bool save_sort = (settings->SaveFlags & SwGuiTableFlags_Sortable) != 0;
        if (!save_size && !save_visible && !save_order && !save_sort)
            continue;

        buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve
        buf->appendf("[%s][0x%08X,%d]\n", handler->TypeName, settings->ID, settings->ColumnsCount);
        if (settings->RefScale != 0.0f)
            buf->appendf("RefScale=%g\n", settings->RefScale);
        SwGuiTableColumnSettings* column = settings->GetColumnSettings();
        for (int column_n = 0; column_n < settings->ColumnsCount; column_n++, column++)
        {
            // "Column 0  UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v"
            bool save_column = column->UserID != 0 || save_size || save_visible || save_order || (save_sort && column->SortOrder != -1);
            if (!save_column)
                continue;
            buf->appendf("Column %-2d", column_n);
            if (column->UserID != 0)                    buf->appendf(" UserID=%08X", column->UserID);
            if (save_size && column->IsStretch)         buf->appendf(" Weight=%.4f", column->WidthOrWeight);
            if (save_size && !column->IsStretch)        buf->appendf(" Width=%d", (int)column->WidthOrWeight);
            if (save_visible)                           buf->appendf(" Visible=%d", column->IsEnabled);
            if (save_order)                             buf->appendf(" Order=%d", column->DisplayOrder);
            if (save_sort && column->SortOrder != -1)   buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == SwGuiSortDirection_Ascending) ? 'v' : '^');
            buf->append("\n");
        }
        buf->append("\n");
    }
}

static size_t TableSettingsCalcChunkSize(int columns_count)
{
    return sizeof(SwGuiTableSettings) + (size_t)columns_count * sizeof(SwGuiTableColumnSettings);
}

SwGuiTableSettings* SW::TableSettingsCreate(SwGuiID id, int columns_count)
{
    SwGuiContext& g = *GMUI_Ctx;
    SwGuiTableSettings* settings = g.SettingsTables.alloc_chunk(TableSettingsCalcChunkSize(columns_count));
    TableSettingsInit(settings, id, columns_count, columns_count);
    return settings;
}

// Find existing settings
SwGuiTableSettings* SW::TableSettingsFindByID(SwGuiID id)
{
    // FIXME-OPT: Might want to store a lookup map for this?
    SwGuiContext& g = *GMUI_Ctx;
    for (SwGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings))
        if (settings->ID == id)
            return settings;
    return NULL;
}

void SW::TableSettingsAddSettingsHandler()
{
    SwGuiSettingsHandler ini_handler;
    ini_handler.TypeName = "Table";
    ini_handler.TypeHash = SwHashStr("Table");
    ini_handler.ClearAllFn = TableSettingsHandler_ClearAll;
    ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen;
    ini_handler.ReadLineFn = TableSettingsHandler_ReadLine;
    ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll;
    ini_handler.WriteAllFn = TableSettingsHandler_WriteAll;
    AddSettingsHandler(&ini_handler);
}

// Free up/compact internal Table buffers for when it gets unused
void SW::TableGcCompactTransientBuffers(SwGuiTable* table)
{
    //SW_DEBUG_PRINT("TableGcCompactTransientBuffers() id=0x%08X\n", table->ID);
    SwGuiContext& g = *GMUI_Ctx;
    SW_ASSERT(table->MemoryCompacted == false);
    table->SortSpecs.Specs = NULL;
    table->SortSpecsMulti.clear();
    table->IsSortSpecsDirty = true; // FIXME: shouldn't have to leak into user performing a sort
    table->ColumnsNames.clear();
    table->MemoryCompacted = true;
    for (int n = 0; n < table->ColumnsCount; n++)
        table->Columns[n].NameOffset = -1;
    g.TablesLastTimeActive[g.Tables.GetIndex(table)] = -1.0f;
}

void SW::TableGcCompactTransientBuffers(SwGuiTableTempData* temp_data)
{
    temp_data->DrawSplitter.ClearFreeMemory();
    temp_data->LastTimeActive = -1.0f;
}

// Compact and remove unused settings data (currently only used by TestEngine)
void SW::TableGcCompactSettings()
{
    SwGuiContext& g = *GMUI_Ctx;
    int required_memory = 0;
    for (SwGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings))
        if (settings->ID != 0)
            required_memory += (int)TableSettingsCalcChunkSize(settings->ColumnsCount);
    if (required_memory == g.SettingsTables.Buf.Size)
        return;
    SwChunkStream<SwGuiTableSettings> new_chunk_stream;
    new_chunk_stream.Buf.reserve(required_memory);
    for (SwGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings))
        if (settings->ID != 0)
            memcpy(new_chunk_stream.alloc_chunk(TableSettingsCalcChunkSize(settings->ColumnsCount)), settings, TableSettingsCalcChunkSize(settings->ColumnsCount));
    g.SettingsTables.swap(new_chunk_stream);
}

static const float COLUMNS_HIT_RECT_HALF_WIDTH = 4.0f;

static float GetDraggedColumnOffset(SwGuiOldColumns* columns, int column_index)
{
	// Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing
	// window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning.
    SwGuiContext& g = *GMUI_Ctx;
    SwGuiWindow* window = g.CurrentWindow;
	SW_ASSERT(column_index > 0); // We are not supposed to drag column 0.
	SW_ASSERT(g.ActiveId == columns->ID + SwGuiID(column_index));

	float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + COLUMNS_HIT_RECT_HALF_WIDTH - window->Pos.x;
	x = SwMax(x, SW::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing);
	if ((columns->Flags & SwGuiOldColumnFlags_NoPreserveWidths))
		x = SwMin(x, SW::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing);

	return x;
}

void SW::EndColumns()
{
    SwGuiContext& g = *GMUI_Ctx;
	SwGuiWindow* window = GetCurrentWindow();
	SwGuiOldColumns* columns = window->DC.CurrentColumns;
	SW_ASSERT(columns != NULL);

	PopItemWidth();
	if (columns->Count > 1)
	{
		PopClipRect();
		columns->Splitter.Merge(window->DrawList);
	}

	const SwGuiOldColumnFlags flags = columns->Flags;
	columns->LineMaxY = SwMax(columns->LineMaxY, window->DC.CursorPos.y);
	window->DC.CursorPos.y = columns->LineMaxY;
	if (!(flags & SwGuiOldColumnFlags_GrowParentContentsSize))
		window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX;  // Restore cursor max pos, as columns don't grow parent

	// Draw columns borders and handle resize
	// The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy
	bool is_being_resized = false;
	if (!(flags & SwGuiOldColumnFlags_NoBorder) && !window->SkipItems)
	{
		// We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.
		const float y1 = SwMax(columns->HostCursorPosY, window->ClipRect.Min.y);
		const float y2 = SwMin(window->DC.CursorPos.y, window->ClipRect.Max.y);
		int dragging_column = -1;
		for (int n = 1; n < columns->Count; n++)
		{
            SwGuiOldColumnData* column = &columns->Columns[n];
			float x = window->Pos.x + GetColumnOffset(n);
			const SwGuiID column_id = columns->ID + SwGuiID(n);
			const float column_hit_hw = COLUMNS_HIT_RECT_HALF_WIDTH;
			const SwRect column_hit_rect(SwVec2(x - column_hit_hw, y1), SwVec2(x + column_hit_hw, y2));
			KeepAliveID(column_id);
			if (IsClippedEx(column_hit_rect, column_id))
				continue;

			bool hovered = false, held = false;
			if (!(flags & SwGuiOldColumnFlags_NoResize))
			{
				ButtonBehavior(column_hit_rect, column_id, &hovered, &held);
				if (hovered || held)
					g.MouseCursor = SwGuiMouseCursor_ResizeEW;
				if (held && !(column->Flags & SwGuiOldColumnFlags_NoResize))
					dragging_column = n;
			}

			// Draw column
			const SwU32 col = GetColorU32(held ? SwGuiCol_SeparatorActive : hovered ? SwGuiCol_SeparatorHovered : SwGuiCol_Separator);
			const float xi = SW_FLOOR(x);
			window->DrawList->AddLine(SwVec2(xi, y1 + 1.0f), SwVec2(xi, y2), col);
		}

		// Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame.
		if (dragging_column != -1)
		{
			if (!columns->IsBeingResized)
				for (int n = 0; n < columns->Count + 1; n++)
					columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm;
			columns->IsBeingResized = is_being_resized = true;
			float x = GetDraggedColumnOffset(columns, dragging_column);
			SetColumnOffset(dragging_column, x);
		}
	}
	columns->IsBeingResized = is_being_resized;

	window->WorkRect = window->ParentWorkRect;
	window->ParentWorkRect = columns->HostBackupParentWorkRect;
	window->DC.CurrentColumns = NULL;
	window->DC.ColumnsOffset.x = 0.0f;
	window->DC.CursorPos.x = SW_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x);
}

float SW::GetColumnOffsetFromNorm(const SwGuiOldColumns* columns, float offset_norm)
{
	return offset_norm * (columns->OffMaxX - columns->OffMinX);
}

float SW::GetColumnNormFromOffset(const SwGuiOldColumns* columns, float offset)
{
	return offset / (columns->OffMaxX - columns->OffMinX);
}

float SW::GetColumnOffset(int column_index)
{
	SwGuiWindow* window = GetCurrentWindowRead();
	SwGuiOldColumns* columns = window->DC.CurrentColumns;
	if (columns == NULL)
		return 0.0f;

	if (column_index < 0)
		column_index = columns->Current;
	SW_ASSERT(column_index < columns->Columns.Size);

	const float t = columns->Columns[column_index].OffsetNorm;
	const float x_offset = SwLerp(columns->OffMinX, columns->OffMaxX, t);
	return x_offset;
}

static float GetColumnWidthEx(SwGuiOldColumns* columns, int column_index, bool before_resize = false)
{
	if (column_index < 0)
		column_index = columns->Current;

	float offset_norm;
	if (before_resize)
		offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize;
	else
		offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm;
	return SW::GetColumnOffsetFromNorm(columns, offset_norm);
}

float SW::GetColumnWidth(int column_index)
{
	SwGuiContext& g = *GMUI_Ctx;
	SwGuiWindow* window = g.CurrentWindow;
	SwGuiOldColumns* columns = window->DC.CurrentColumns;
	if (columns == NULL)
		return GetContentRegionAvail().x;

	if (column_index < 0)
		column_index = columns->Current;
	return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm);
}

void SW::SetColumnWidth(int column_index, float width)
{
	SwGuiWindow* window = GetCurrentWindowRead();
	SwGuiOldColumns* columns = window->DC.CurrentColumns;
	SW_ASSERT(columns != NULL);

	if (column_index < 0)
		column_index = columns->Current;
	SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width);
}

void SW::SetColumnOffset(int column_index, float offset)
{
	SwGuiContext& g = *GMUI_Ctx;
	SwGuiWindow* window = g.CurrentWindow;
	SwGuiOldColumns* columns = window->DC.CurrentColumns;
	SW_ASSERT(columns != NULL);

	if (column_index < 0)
		column_index = columns->Current;
	SW_ASSERT(column_index < columns->Columns.Size);

	const bool preserve_width = !(columns->Flags & SwGuiOldColumnFlags_NoPreserveWidths) && (column_index < columns->Count - 1);
	const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f;

	if (!(columns->Flags & SwGuiOldColumnFlags_NoForceWithinWindow))
		offset = SwMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index));
	columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX);

	if (preserve_width)
		SetColumnOffset(column_index + 1, offset + SwMax(g.Style.ColumnsMinSpacing, width));
}

// [Internal] Small optimization to avoid calls to PopClipRect/SetCurrentChannel/PushClipRect in sequences,
// they would meddle many times with the underlying SwDrawCmd.
// Instead, we do a preemptive overwrite of clipping rectangle _without_ altering the command-buffer and let
// the subsequent single call to SetCurrentChannel() does it things once.
void SW::SetWindowClipRectBeforeSetChannel(SwGuiWindow* window, const SwRect& clip_rect)
{
    SwVec4 clip_rect_vec4 = clip_rect.ToVec4();
    window->ClipRect = clip_rect;
    window->DrawList->_CmdHeader.ClipRect = clip_rect_vec4;
    window->DrawList->_ClipRectStack.Data[window->DrawList->_ClipRectStack.Size - 1] = clip_rect_vec4;
}

void SW::PopColumnsBackground()
{
    SwGuiWindow* window = GetCurrentWindowRead();
    SwGuiOldColumns* columns = window->DC.CurrentColumns;
    if (columns->Count == 1)
        return;

    // Optimization: avoid PopClipRect() + SetCurrentChannel()
    SetWindowClipRectBeforeSetChannel(window, columns->HostBackupClipRect);
    columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1);
}

// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns)
void SW::PushColumnsBackground()
{
    SwGuiWindow* window = GetCurrentWindowRead();
    SwGuiOldColumns* columns = window->DC.CurrentColumns;
    if (columns->Count == 1)
        return;

    // Optimization: avoid SetCurrentChannel() + PushClipRect()
    columns->HostBackupClipRect = window->ClipRect;
    SetWindowClipRectBeforeSetChannel(window, columns->HostInitialClipRect);
    columns->Splitter.SetCurrentChannel(window->DrawList, 0);
}


//-------------------------------------------------------------------------
// [SECTION] Tables: Drawing
//-------------------------------------------------------------------------
// - TablePushBackgroundChannel() [Internal]
// - TablePopBackgroundChannel() [Internal]
// - TableSetupDrawChannels() [Internal]
// - TableMergeDrawChannels() [Internal]
// - TableDrawBorders() [Internal]
//-------------------------------------------------------------------------

// Bg2 is used by Selectable (and possibly other widgets) to render to the background.
// Unlike our Bg0/1 channel which we uses for RowBg/CellBg/Borders and where we guarantee all shapes to be CPU-clipped, the Bg2 channel being widgets-facing will rely on regular ClipRect.
void SW::TablePushBackgroundChannel()
{
    SwGuiContext& g = *GMUI_Ctx;
    SwGuiWindow* window = g.CurrentWindow;
    SwGuiTable* table = g.CurrentTable;

    // Optimization: avoid SetCurrentChannel() + PushClipRect()
    table->HostBackupInnerClipRect = window->ClipRect;
    SetWindowClipRectBeforeSetChannel(window, table->Bg2ClipRectForDrawCmd);
    table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Bg2DrawChannelCurrent);
}

void SW::TablePopBackgroundChannel()
{
    SwGuiContext& g = *GMUI_Ctx;
    SwGuiWindow* window = g.CurrentWindow;
    SwGuiTable* table = g.CurrentTable;
    SwGuiTableColumn* column = &table->Columns[table->CurrentColumn];

    // Optimization: avoid PopClipRect() + SetCurrentChannel()
    SetWindowClipRectBeforeSetChannel(window, table->HostBackupInnerClipRect);
    table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent);
}